/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// sv_main.cpp - main server file
//


#include "sv_local.h"


serverActive_t				sv;
serverStatic_t				svs;

cvar_t *					sv_padPackets;
cvar_t *					sv_showRate;
cvar_t *					sv_serverId;
cvar_t *					sv_map;
cvar_t *					sv_pure;
cvar_t *					sv_allowCheats;
cvar_t *					sv_voiceChat;
cvar_t *					sv_numClients;
cvar_t *					sv_maxClients;
cvar_t *					sv_minPing;
cvar_t *					sv_maxPing;
cvar_t *					sv_maxRate;
cvar_t *					sv_password;
cvar_t *					sv_needPassword;
cvar_t *					sv_floodProtect;
cvar_t *					sv_lanServer;
cvar_t *					sv_clientTimeOut;
cvar_t *					sv_zombieTimeOut;
cvar_t *					sv_reconnectLimit;


/*
 ==============================================================================

 CLIENT RELIABLE COMMAND

 ==============================================================================
*/


/*
 ==================
 SV_AddReliableCommand

 The given command will be sent to the client
 ==================
*/
void SV_AddReliableCommand (client_t *client, const char *fmt, ...){

	char	command[MAX_STRING_LENGTH];
	va_list	argPtr;
	int		index;
	int		len;

	if (client->state < CS_CONNECTED)
		return;

	va_start(argPtr, fmt);
	len = Str_VSPrintf(command, sizeof(command), fmt, argPtr);
	va_end(argPtr);

	if (len == -1)
		Com_Error(false, "SV_AddReliableCommand: command string overflow");

	// If we would be losing an old command that hasn't been acknowledged, we
	// must drop the connection
	if (client->reliableCommandSequence - client->reliableCommandAcknowledge == MAX_RELIABLE_COMMANDS){
		SV_DropClient(client, "Overflowed reliable command buffer");
		return;
	}

	// Store it off
	client->reliableCommandSequence++;

	index = client->reliableCommandSequence & (MAX_RELIABLE_COMMANDS - 1);
	Str_Copy(client->reliableCommandList[index], command, sizeof(client->reliableCommandList[index]));
}

/*
 ==================
 SV_BroadcastReliableCommand

 The given command will be broadcast to all clients
 ==================
*/
void SV_BroadcastReliableCommand (const char *fmt, ...){

	client_t	*client;
	char		command[MAX_STRING_LENGTH];
	va_list		argPtr;
	int			index;
	int			i, len;

	va_start(argPtr, fmt);
	len = Str_VSPrintf(command, sizeof(command), fmt, argPtr);
	va_end(argPtr);

	if (len == -1)
		Com_Error(false, "SV_BroadcastReliableCommand: command string overflow");

	// Echo broadcast prints to the system console
	if (com_dedicated->integerValue && !Str_ICompareChars(command, "print ", 6))
		Com_Printf("Broadcast: %s\n", command + 6);

	// Send the command to all relevant clients
	for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
		if (client->state < CS_PRIMED)
			continue;

		// If we would be losing an old command that hasn't been acknowledged,
		// we must drop the connection
		if (client->reliableCommandSequence - client->reliableCommandAcknowledge == MAX_RELIABLE_COMMANDS){
			SV_DropClient(client, "Overflowed reliable command buffer");
			continue;
		}

		// Store it off
		client->reliableCommandSequence++;

		index = client->reliableCommandSequence & (MAX_RELIABLE_COMMANDS - 1);
		Str_Copy(client->reliableCommandList[index], command, sizeof(client->reliableCommandList[index]));
	}
}


/*
 ==============================================================================

 FUNCTIONS USED BY THE FRAMEWORK

 ==============================================================================
*/


/*
 ==================
 SV_CheckTimeOuts

 If a packet has not been received from a client for sv_clientTimeOut seconds,
 drop the connection. Server time is used instead of real time to avoid
 dropping the local client while debugging.

 When a client is normally dropped, the client goes into a zombie state for a
 few seconds to make sure any final reliable message gets resent if necessary.
 ==================
*/
static void SV_CheckTimeOuts (){

	client_t	*client;
	int			clientTimeOut, zombieTimeOut;
	int			i;

	clientTimeOut = SEC2MS(sv_clientTimeOut->floatValue);
	zombieTimeOut = SEC2MS(sv_zombieTimeOut->floatValue);

	for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
		if (client->state == CS_FREE)
			continue;

		// Message times may be wrong accross a level change
		if (client->lastReceivedTime > svs.time)
			client->lastReceivedTime = svs.time;

		if (client->state == CS_ZOMBIE && svs.time - client->lastReceivedTime > zombieTimeOut){
			// Free the client slot
			Com_DPrintf("Going to CS_FREE for %s\n", client->playerName);

			client->state = CS_FREE;	// Can now be reused

			// Update number of clients for server info
			CVar_SetInteger(sv_numClients, sv_numClients->integerValue - 1);

			// If this was the last client on the server, send a heartbeat to
			// the master server immediately
			if (!sv_numClients->integerValue)
				svs.masterHeartbeatTime = svs.time - HEARTBEAT_TIMEOUT;

			continue;
		}

		if (client->state >= CS_CONNECTED && svs.time - client->lastReceivedTime > clientTimeOut){
			if (++client->timeOutCount == 5){
				SV_DropClient(client, "Timed out");

				// Don't bother with zombie state
				Com_DPrintf("Going to CS_FREE for %s\n", client->playerName);

				client->state = CS_FREE;	// Can now be reused

				// Update number of clients for server info
				CVar_SetInteger(sv_numClients, sv_numClients->integerValue - 1);

				// If this was the last client on the server, send a heartbeat
				// to the master server immediately
				if (!sv_numClients->integerValue)
					svs.masterHeartbeatTime = svs.time - HEARTBEAT_TIMEOUT;
			}

			continue;
		}

		client->timeOutCount = 0;
	}
}

/*
 ==================
 SV_UpdatePings

 Updates ping for all clients. This is also used by the server-game for snapshot
 writing to the client-game
 ==================
*/
static void SV_UpdatePings (){

	client_t	*client;
	int			count, total;
	int			i, j;

	for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
		if (client->state < CS_CONNECTED)
			continue;

		// Calculate ping
		if (client->state != CS_ACTIVE)
			client->ping = 999;
		else {
			count = 0;
			total = 0;

			for (j = 0; j < PACKET_BACKUP; j++){
				if (client->packets[j].timeAcked <= 0)
					continue;

				count++;
				total += (client->packets[j].timeAcked - client->packets[j].timeSent);
			}

			if (!count)
				client->ping = 999;
			else {
				client->ping = total / count;
				if (client->ping > 999)
					client->ping = 999;
			}
		}

		// Let the game module know about the ping
		glqSGame->ClientSetPing(client->clientNum, client->ping);
	}
}

/*
 ==================
 SV_MasterHeartbeat

 Sends a heartbeat to the master server every few minutes to let it know we are
 alive
 ==================
*/
static void SV_MasterHeartbeat (){

	if (sv_lanServer->integerValue)
		return;		// Don't send a heartbeat if we are a LAN server

	if (svs.time - svs.masterHeartbeatTime < HEARTBEAT_TIMEOUT)
		return;		// Don't send a heartbeat yet

	svs.masterHeartbeatTime = svs.time;

	// Resolve master server address if needed
	if (!svs.masterResolved){
		svs.masterResolved = true;

		Com_Printf("Resolving master server address...\n");

		if (!NET_StringToAddress(MASTER_SERVER, &svs.masterAddress)){
			Com_Printf("Couldn't resolve %s\n", MASTER_SERVER);
			return;
		}

		if (svs.masterAddress.port == 0)
			svs.masterAddress.port = BigShort(PORT_MASTER);

		Com_Printf("%s resolved to %s\n", MASTER_SERVER, NET_AddressToString(svs.masterAddress));
	}

	if (svs.masterAddress.port == 0)
		return;		// Master server is down

	// Send a heartbeat to the master server
	Com_DPrintf("Sending heartbeat to master server...\n");

	NET_OutOfBandPrintf(NS_SERVER, svs.masterAddress, "heartbeat %s %s %i", ENGINE_NAME, CVar_GetVariableString("fs_game"), PROTOCOL_VERSION);
}


// ============================================================================


/*
 ==================
 SV_FinalMessage

 Used by SV_Shutdown to send a final message to all connected clients before
 the server goes down. The messages are sent immediately because the server is
 going to totally exit after returning from this function.
 ==================
*/
static void SV_FinalMessage (const char *message){

	client_t	*client;
	int			i, j;

	if (!svs.clients)
		return;

	// Send it twice in case one is dropped
	for (i = 0; i < 2; i++){
		for (j = 0, client = svs.clients; j < sv_maxClients->integerValue; j++, client++){
			if (client->state < CS_CONNECTED)
				continue;

			// Don't send a disconnect message to the local client
			if (NET_IsLocalAddress(client->netChan.remoteAddress))
				continue;

			SV_AddReliableCommand(client, "disconnect %s", message);

			// Force a new packet to be sent
			SV_SendPacketToClient(client, true);
		}
	}
}

/*
 ==================
 SV_PrintStats
 ==================
*/
static void SV_PrintStats (){

	client_t	*client;
	int			totalIncoming, totalOutgoing;
	int			i;

	if (sv_showRate->integerValue){
		if (sv_showRate->integerValue == 2){
			for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
				if (client->state < CS_CONNECTED)
					continue;

				Com_Printf("client: %2i, incoming rate: %4i KB/sec, outgoing rate: %4i KB/sec\n", i, SIZE_KB(client->netChan.incomingRateBytes), SIZE_KB(client->netChan.outgoingRateBytes));
			}
		}
		else {
			totalIncoming = 0;
			totalOutgoing = 0;

			for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
				if (client->state < CS_CONNECTED)
					continue;

				totalIncoming += client->netChan.incomingRateBytes;
				totalOutgoing += client->netChan.outgoingRateBytes;
			}

			Com_Printf("total incoming rate: %4i KB/sec, total outgoing rate: %4i KB/sec\n", SIZE_KB(totalIncoming), SIZE_KB(totalOutgoing));
		}
	}
}


/*
 ==============================================================================

 FRAMEWORK

 ==============================================================================
*/


/*
 ==================
 SV_Frame
 ==================
*/
void SV_Frame (int msec){

	int		timeServer, timeWaiting;

	// If not running, dump any incoming packets
	if (!svs.initialized){
		SV_ReadPackets(true);
		return;
	}

	if (com_speeds->integerValue)
		timeServer = Sys_Milliseconds();

	// Read and parse incoming packets
	SV_ReadPackets(false);

	// If it isn't time for a new frame, do nothing
	sv.timeResidual += msec;

	if (sv.timeResidual < GAME_FRAMEMSEC){
		// Give the game a chance to redraw debug tools and integrated editors
		glqSGame->Redraw();

		if (com_speeds->integerValue)
			com_timeServer += (Sys_Milliseconds() - timeServer);

		// If running a dedicated server, sleep until either a packet arrives
		// from the network or time enough for a frame has gone by
		if (com_dedicated->integerValue){
			if (com_speeds->integerValue)
				timeWaiting = Sys_Milliseconds();

			NET_WaitForPackets(NS_SERVER, GAME_FRAMEMSEC - sv.timeResidual);

			if (com_speeds->integerValue)
				com_timeWaiting += (Sys_Milliseconds() - timeWaiting);
		}

		return;
	}

	// If svs.time is about to hit the 32nd bit, kick all clients and clear it,
	// rather than checking for negative time wrap-around everywhere
	if (svs.time > 0x70000000){
		// Buffer a map command for a few frames so the server is restarted
		if (sv_allowCheats->integerValue)
			Cmd_AppendText(Str_VarArgs("wait 10 ; devMap %s", sv_map->value));
		else
			Cmd_AppendText(Str_VarArgs("wait 10 ; map %s", sv_map->value));

		// Kill the server and drop the client connection
		SV_Shutdown("Restarting server due to time wrapping");
		CL_Drop("Restarting server due to time wrapping");

		return;
	}

	// Check if a client connection timed out
	SV_CheckTimeOuts();

	// Update pings for all connected clients
	SV_UpdatePings();

	// Run the game simulation in chunks
	while (sv.timeResidual >= GAME_FRAMEMSEC){
		sv.timeResidual -= GAME_FRAMEMSEC;

		svs.time += GAME_FRAMEMSEC;

		glqSGame->RunFrame();
	}

	// Give the game a chance to redraw debug tools and integrated editors
	glqSGame->Redraw();

	// Send packets back to the clients
	SV_SendClientPackets();

	// Send a heartbeat to the master server if needed
	SV_MasterHeartbeat();

	// Print statistics
	SV_PrintStats();

	if (com_speeds->integerValue)
		com_timeServer += (Sys_Milliseconds() - timeServer);
}


/*
 ==============================================================================

 INITIALIZATION & SHUTDOWN

 ==============================================================================
*/


/*
 ==================
 
 Called only from Com_Init, not for each game
 ==================
*/
void SV_Init (){

	// Register variables
	sv_padPackets = CVar_Register("sv_padPackets", "0", CVAR_INTEGER, CVAR_CHEAT, "Pad packets for rate debugging", 0, MAX_MSG_SIZE);
	sv_showRate = CVar_Register("sv_showRate", "0", CVAR_INTEGER, CVAR_CHEAT, "Show network rate (1 = totals, 2 = per-client)", 0, 2);
	sv_serverId = CVar_Register("sv_serverId", "0", CVAR_INTEGER, CVAR_SERVERINFO | CVAR_READONLY, "Server identification", 0, 0);
	sv_map = CVar_Register("sv_map", "", CVAR_STRING, CVAR_SERVERINFO | CVAR_READONLY, "Map being played", 0, 0);
	sv_pure = CVar_Register("sv_pure", "1", CVAR_BOOL, CVAR_SERVERINFO | CVAR_LATCH, "Pure server (don't allow modified data on clients)", 0, 0);	
	sv_allowCheats = CVar_Register("sv_allowCheats", "0", CVAR_BOOL, CVAR_SERVERINFO | CVAR_READONLY, "Allow cheats in the game", 0, 0);	
	sv_voiceChat = CVar_Register("sv_voiceChat", "1", CVAR_BOOL, CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_LATCH, "Enable voice chat on the server", 0, 0);
	sv_numClients = CVar_Register("sv_numClients", "0", CVAR_INTEGER, CVAR_SERVERINFO | CVAR_READONLY, "Number of clients currently on the server", 0, MAX_CLIENTS);	
	sv_maxClients = CVar_Register("sv_maxClients", "32", CVAR_INTEGER, CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_LATCH, "Maximum number of clients allowed on the server", 4, MAX_CLIENTS);
	sv_minPing = CVar_Register("sv_minPing", "0", CVAR_INTEGER, CVAR_ARCHIVE | CVAR_SERVERINFO, "Minimum ping allowed for clients when connecting (0 = unlimited)", 0, 999);
	sv_maxPing = CVar_Register("sv_maxPing", "0", CVAR_INTEGER, CVAR_ARCHIVE | CVAR_SERVERINFO, "Maximum ping allowed for clients when connecting (0 = unlimited)", 0, 999);	
	sv_maxRate = CVar_Register("sv_maxRate", "100000", CVAR_INTEGER, CVAR_ARCHIVE | CVAR_SERVERINFO, "Maximum rate allowed for internet clients", 5000, 100000);	
	sv_password = CVar_Register("sv_password", "", CVAR_STRING, CVAR_ARCHIVE, "Server password", 0, 0);
	sv_needPassword = CVar_Register("sv_needPassword", "0", CVAR_BOOL, CVAR_ARCHIVE | CVAR_SERVERINFO, "Require a password to all clients when connecting", 0, 0);	
	sv_floodProtect = CVar_Register("sv_floodProtect", "1", CVAR_BOOL, CVAR_SERVERINFO, "Flood protection", 0, 0);	
	sv_lanServer = CVar_Register("sv_lanServer", "0", CVAR_BOOL, CVAR_LATCH, "LAN server (don't allow internet clients)", 0, 0);
	sv_clientTimeOut = CVar_Register("sv_clientTimeOut", "180.0", CVAR_FLOAT, 0, "Client time-out time in seconds", 1.0f, 300.0f);
	sv_zombieTimeOut = CVar_Register("sv_zombieTimeOut", "2.0", CVAR_FLOAT, 0, "Zombie time-out time in seconds", 1.0f, 300.0f);	
	sv_reconnectLimit = CVar_Register("sv_reconnectLimit", "3.0", CVAR_FLOAT, 0, "Time in seconds before a client is allowed to reconnect", 1.0f, 300.0f);

	// Add commands
	Cmd_AddCommand("map", SV_Map_f, "Loads a map", Cmd_ArgCompletion_MapName);
	Cmd_AddCommand("devMap", SV_DevMap_f, "Loads a map in developer mode", Cmd_ArgCompletion_MapName);
	Cmd_AddCommand("loadGame", SV_LoadGame_f, "Loads a game", NULL);
	Cmd_AddCommand("saveGame", SV_SaveGame_f, "Saves a game", NULL);
}

/*
 ==================
 
 Called from Com_Shutdown, but also when each game quits or an error is thrown
 ==================
*/
void SV_Shutdown (const char *message){

	if (!svs.initialized)
		return;		// Nothing running on the server

	Com_Printf("-------- Server Shutdown --------\n");

	// Send a final message
	SV_FinalMessage(message);

	// Shutdown game module
	SG_Shutdown();

	// Wipe the entire serverActive_t structure
	Mem_Fill(&sv, 0, sizeof(serverActive_t));

	// Free clients

	// Wipe the entire serverStatic_t structure
	Mem_Fill(&svs, 0, sizeof(serverStatic_t));

	// Free the clip map
	CM_FreeMap();

	// Allow cheats
	CVar_FixCheatVariables(true);

	// Clear variables
	CVar_SetString(sv_map, "");

	// Reset the com_serverRunning variable
	CVar_SetBool(com_serverRunning, false);

	Com_Printf("---------------------------------\n");
}